package net.sourceforge.jradiusclient;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import net.sourceforge.jradiusclient.exception.InvalidParameterException;
import net.sourceforge.jradiusclient.exception.RadiusException;

/**
 * Released under the LGPL<BR>
 * 
 * @author <a href="mailto:bloihl@users.sourceforge.net">Robert J. Loihl</a>
 * @version $Revision: 1.11 $
 */
public class RadiusPacket {
	public static final int MIN_PACKET_LENGTH = 20;

	public static final int MAX_PACKET_LENGTH = 4096;

	/**
	 * RADIUS_HEADER_LENGTH is 20 bytes (corresponding to 1 byte for code + 1
	 * byte for Identifier + 2 bytes for Length + 16 bytes for Request
	 * Authenticator) It is not a coincidence that it is the same as the
	 * MIN_PACKET_LENGTH
	 */
	public static final short RADIUS_HEADER_LENGTH = 20;

	public static final String EMPTYSTRING = "";

	/* *************** Constant Packet Type Codes ************************* */
	public static final int ACCESS_REQUEST = 1;

	public static final int ACCESS_ACCEPT = 2;

	public static final int ACCESS_REJECT = 3;

	public static final int ACCOUNTING_REQUEST = 4;

	public static final int ACCOUNTING_RESPONSE = 5;

	public static final int ACCOUNTING_STATUS = 6;

	public static final int PASSWORD_REQUEST = 7;

	public static final int PASSWORD_ACCEPT = 8;

	public static final int PASSWORD_REJECT = 9;

	public static final int ACCOUNTING_MESSAGE = 10;

	public static final int ACCESS_CHALLENGE = 11;

	public static final int STATUS_SERVER = 12; // experimental

	public static final int STATUS_CLIENT = 13; // experimental

	public static final int RESERVED = 255;

	/* ****************** Constant Packet Type Codes ************************ */
	private static Object nextPacketIdLock = new Object();

	private static byte nextPacketId = (byte) 0;

	private int packetType = 0;

	private byte packetIdentifier = (byte) 0;

	private List attributes;

	/**
	 * builds a type RadiusPacket with no Attributes set
	 * 
	 * @param type
	 *            int a PacketType to send.
	 * @throws InvalidParameterException
	 *             if the attributeList is null or contains non-RadiusAttribute
	 *             type entries
	 */
	public RadiusPacket(final int type) throws InvalidParameterException {
		this(type, getAndIncrementPacketIdentifier(), new ArrayList());
	}

	/**
	 * 
	 * @param type
	 * @param identifier
	 * @throws InvalidParameterException
	 */
	public RadiusPacket(final int type, final byte identifier)
			throws InvalidParameterException {
		this(type, identifier, new ArrayList());
	}

	/**
	 * Builds a RadiusPacket with a predefined set of attributes
	 * 
	 * @param type
	 *            int a PacketType to send.
	 * @param attributeList
	 *            a list of RadiusAttribute objects to initialize this
	 *            RadiusPacket with
	 * @throws InvalidParameterException
	 *             if the attributeList is null or contains non-RadiusAttribute
	 *             type entries
	 */
	public RadiusPacket(final int type, final List attributeList)
			throws InvalidParameterException {
		this(type, getAndIncrementPacketIdentifier(), attributeList);
	}

	/**
	 * Builds a RadiusPacket with a predefined set of attributes
	 * 
	 * @param type
	 * @param identifier
	 * @param attributeList
	 * @throws InvalidParameterException
	 */
	public RadiusPacket(final int type, final byte identifier,
			final List attributeList) throws InvalidParameterException {
		if ((type < 1) || (type > 256)) {
			throw new InvalidParameterException("Type was out of bounds");
		}
		if (null == attributeList) {
			throw new InvalidParameterException("Attribute List was null");
		}
		this.packetType = type;
		this.packetIdentifier = identifier;
		this.attributes = new ArrayList();
		this.setAttributes(attributeList);
	}

	/**
	 * Adds a RadiusAttribute to this RadiusPacket
	 * 
	 * @param radiusAttribute
	 *            A RadiusAttribute to set on this RadiusPacket
	 * @throws InvalidParameterException
	 *             if the parameter radiusAttribute was null
	 */
	public void setAttribute(RadiusAttribute radiusAttribute)
			throws InvalidParameterException {
		if (null == radiusAttribute) {
			throw new InvalidParameterException("radiusAttribute was null");
		}
		validateAttribute(radiusAttribute);
		synchronized (this.attributes) {
			this.attributes.add(radiusAttribute);
		}
	}

	/**
	 * Add a set of RadiusAttributes to this RadiusPacket
	 * 
	 * @param attributeList
	 *            a list of RadiusAttribute objects to add to this RadiusPacket
	 * @throws InvalidParameterException
	 *             if the attributeList is null or contains non-RadiusAttribute
	 *             type entries
	 */
	public void setAttributes(final List attributeList)
			throws InvalidParameterException {
		if (null == attributeList) {
			throw new InvalidParameterException("Attribute List was null");
		}
		RadiusAttribute tempRa;
		Iterator iter = attributeList.iterator();
		while (iter.hasNext()) {
			try {
				tempRa = (RadiusAttribute) iter.next();
				validateAttribute(tempRa);
			} catch (ClassCastException ccex) {
				throw new InvalidParameterException(
						"Attribute List contained an entry that was not a net.sourceforge.jradiusclient.RadiusAttribute");
			}
			synchronized (this.attributes) {
				this.attributes.add(tempRa);
			}
		}
	}

	// do nothing method, sub-classes should implement to get attributes
	// validation during
	// the setAttribute and setAttributes methods
	protected void validateAttribute(final RadiusAttribute attribute)
			throws InvalidParameterException {

	}

	/**
	 * retrieve a RadiusAttribute from this RadiusPacket
	 * 
	 * @param attributeType
	 *            an integer between 0 and 256 (i.e. a byte) from the list of
	 *            Radius constants in net.sourceforge.jradiusclient.RadiusValues
	 * @return a single RadiusAttribute from the RadiusPacket
	 * @throws RadiusException
	 *             if no attribute of type attributeType is stored in this
	 *             RadiusPacket
	 * @throws InvalidParameterException
	 *             if the attributeType is not between 0 and 256 (i.e. a byte)
	 */
	public RadiusAttribute getAttribute(int attributeType)
			throws InvalidParameterException {
		if ((attributeType < 0) || (attributeType > 256)) {
			throw new InvalidParameterException(
					"attributeType is out of bounds");
		}
		RadiusAttribute tempRa = null;
		synchronized (this.attributes) {
			Iterator attributeList = this.attributes.iterator();
			while (attributeList.hasNext()) {
				tempRa = (RadiusAttribute) attributeList.next();
				if (tempRa.getType() == attributeType) {
					return tempRa;
				}
			}
		}
		return null;
	}

	public RadiusAttribute removeAttribute(int attributeType)
			throws InvalidParameterException {
		if ((attributeType < 0) || (attributeType > 256)) {
			throw new InvalidParameterException(
					"attributeType is out of bounds");
		}
		RadiusAttribute tempRa = null;
		synchronized (this.attributes) {
			Iterator attributeList = this.attributes.iterator();
			while (attributeList.hasNext()) {
				tempRa = (RadiusAttribute) attributeList.next();
				if (tempRa.getType() == attributeType) {
					attributeList.remove();
					return tempRa;
				}
			}
		}
		return null;
	}

	public RadiusAttribute[] getAttributes(int attributeType)
			throws InvalidParameterException {
		if ((attributeType < 0) || (attributeType > 256)) {
			throw new InvalidParameterException(
					"attributeType is out of bounds");
		}
		List list = new ArrayList();
		RadiusAttribute tempRa = null;
		synchronized (this.attributes) {
			Iterator attributeList = this.attributes.iterator();
			while (attributeList.hasNext()) {
				tempRa = (RadiusAttribute) attributeList.next();
				if (tempRa.getType() == attributeType) {
					list.add(tempRa);
				}
			}
		}
		RadiusAttribute[] result = new RadiusAttribute[list.size()];
		for (int i = 0; i < result.length; i++) {
			result[i] = (RadiusAttribute) list.get(i);
		}
		return result;
	}

	/**
	 * get all of the RadiusAttributes in this RadiusPacket
	 * 
	 * @return a java.util.Collection of RadiusAttributes
	 */
	public Collection getAttributes() {
		// I am concerned about handing out a Collection that is backed by the
		// attributes map,
		// i.e. changes to our own internal provate data can happen this way!!!!
		return this.attributes;
	}

	/**
	 * get the packet type for this RadiusPacket
	 * 
	 * @return packet type for this RadiusPacket
	 */
	public byte getPacketType() {
		return (byte) this.packetType;
	}

	/**
	 * Return the packetIdentifier for this RadiusPacket. This can be used to
	 * match request packets to response packets
	 * 
	 * @return the packet identifier for this object.
	 */
	public byte getPacketIdentifier() {
		return this.packetIdentifier;
	}

	/**
	 * get the byte array
	 * 
	 * @return a byte array of the raw bytes for all of the RadiusAttributes
	 *         assigned to this RadiusPacket
	 * @throws RadiusException
	 *             If there is any error assembling the bytes into a byte array
	 */
	protected final byte[] getAttributeBytes() throws RadiusException {
		// check for an empty packet
		ByteArrayOutputStream bytes = new ByteArrayOutputStream();
		synchronized (this.attributes) {
			Iterator attributeList = this.attributes.iterator();
			while (attributeList.hasNext()) {
				try {
					bytes.write(((RadiusAttribute) attributeList.next())
							.getBytes());
				} catch (java.io.IOException ioex) {
					throw new RadiusException(
							"Error writing bytes to ByteArrayOutputStream!!!");
				}
			}
			return bytes.toByteArray();
		}
	}

	/**
	 * retrieves the next PacketIdentifier to use and increments the static
	 * storage
	 * 
	 * @return the next packetIdentifier to use.
	 */
	private static byte getAndIncrementPacketIdentifier() {
		synchronized (nextPacketIdLock) {
			return nextPacketId++;
		}
	}
	// /**
	// * This method returns the bytes sent in the STATE attribute of the RADIUS
	// * Server's response to a request
	// *@return java.lang.String the challenge message to display to the user
	// *@exception net.sourceforge.jradiusclient.exception.RadiusException
	// */
	// private byte[] getStateAttributeFromResponse() throws RadiusException{
	// if(this.responseAttributes == null){
	// throw new RadiusException("No Response Attributes have been set.");
	// }
	// byte[] stateBytes = (byte[])this.responseAttributes.get(new
	// Integer(RadiusAttributeValues.STATE));
	// if ((stateBytes == null) || (stateBytes.length == 0)){
	// throw new RadiusException("No State Attribute has been set.");
	// }
	// return stateBytes;
	// }
}